/* SiteParameters.java
 * 
 * PROGRAMMER:    Jeffrey T. Darlington
 * DATE:          December 8, 2009
 * PROJECT:       Cryptnos (Android)
 * ANDROID V.:	  1.1
 * REQUIRES:      
 * REQUIRED BY:   
 * 
 * The SiteParameters object encapsulates an atomic set of Cryptnos parameters
 * for generating a secure passphrase for a given site.  Note that in every
 * case, one key element is *NEVER* stored:  the user's secret passphrase.
 * If you consider a two-factor authentication system, there is something that
 * you have and something that you know.  Only when the two items are combined
 * will the system be unlocked.  Typically, the "something that you know" item
 * is a secret passphrase that the user memorizes, while the "something that
 * you have" item is a physical object like a smart card or random(-ish)
 * number token.  In terms of client-side certificates, the certificate becomes
 * the "have" object and its passphrase the "know".  In this case, the
 * SiteParamemters object becomes the "have", storing all the information
 * needed to generate the Cryptnos passphrase *EXCEPT* for the user's secret
 * passphrase itself.  That becomes the "know" portion, and only when the two
 * are combined can the Cryptnos passphrase be generated.
 * 
 * The site parameters consist of:
 * 		* A site "token" or name, which may be a domain name or another other
 * 		  mnemonic that uniquely identifies what the generated passphrase
 * 		  will be used for.
 * 		* A cryptographic hash algorithm used to generate the passphrase.
 * 		* The number of iterations of the hash algorithm to perform.  This
 * 		  must be a positive integer greater than zero (i.e. at least one).
 * 		* A "character type" value, indicating what characters should or
 * 		  should not be removed from the hash results for the final value.
 * 		* A character limit, indicating a passphrase length restriction.  If
 * 		  no restriction is being imposed, this should be -1.
 * 
 * The site parameters are designed to be stored in an encrypted format.
 * After a new SiteParameters object has been created, it should be stored as
 * a combination of the site "key" and as an encrypted blob containing the
 * rest of the data.  The "key" is a cryptographic hash of the site name plus
 * a salt.  It provides a means of looking up the parameters by site name
 * without storing the site name in the clear.  The key can be obtained from
 * the site name by calling the static generateKeyFromSite() or, if the object
 * has been already created, the getKey() accessor method.  The encrypted form
 * of the data can be obtained by calling the exportEncryptedString() method,
 * which returns a Base64-encoded string containing the encrypted data.  These
 * two values can then be stored in the database with relative safety.  To
 * restore the data, pass the site key and the encrypted string into the
 * second SiteParameters constructor to reconstitute the object.
 * 
 * There is also a second export method that returns an unencrypted string.
 * It is intended primarily as a means of compacting the parameter data into
 * a single string and passing that to the export functionality.  Similarly,
 * another constructor takes the string generated by this method so the object
 * can be rebuilt.  While the existing encrypted export/import methods above
 * could have easily filled this role, I felt it was a built redundant to
 * encrypt the data twice--once coming out of this object and once for the
 * final export file.  Don't use this method and constructor for any other
 * purpose.
 * 
 * For code reuse and modularization, the actual code to generate a Cryptnos
 * password is also contained in this.  To generate the password, create a
 * new SiteParameters object, then call the generatePassword() method, passing
 * in a String containing the user's secret passphrase.  This method will
 * combine all the parameters with the secret and return the generated
 * password, complete with all the restrictions.  Any exceptions thrown should
 * be suitable for throwing into a Toast or error dialog.
 * 
 * UPDATES FOR 1.1.0:  Added the "default" constructor that creates an empty
 * SiteParameters object for the use with the new cross-platform export format.
 * 
 * UPDATES FOR 1.2.0:  Added text encoding information everywhere we can to
 * try and fix some encoding related issues.  Password generation text-to-binary
 * and binary-to-text operations should use CryptnosApplication.getTextEncoding()
 * for conversions, rather than relying on hard-coded constants or the system
 * default.  Anything dealing with import/export, however, should force UTF-8.
 * 
 * This program is Copyright 2010, Jeffrey T. Darlington.
 * E-mail:  android_support@cryptnos.com
 * Web:     http://www.cryptnos.com/
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of
 * the GNU General Public License as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See theGNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
*/
package com.gpfcomics.android.cryptnos;

import java.io.ByteArrayOutputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.spec.AlgorithmParameterSpec;
import java.util.regex.Pattern;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.RIPEMD160Digest;
import org.bouncycastle.crypto.digests.TigerDigest;
import org.bouncycastle.crypto.digests.WhirlpoolDigest;
import org.bouncycastle.util.encoders.Base64;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

/**
 * Encapsulates an atomic set of site parameters for Cryptnos.
 * @author Jeffrey T. Darlington
 * @version 1.0
 * @since 1.0
 */
public class SiteParameters {

	/** The site name or token. */
	private String site = null;
	/** The cryptographic hash to use to generate the passphrase. */
	private String hash = "SHA-1";
	/** The number of iterations of the cryptographic hash to perform. */
	private int iterations = 1;
	/** The type of characters to include/exclude from the final passphrase. */
	private int charTypes = 0;
	/** A limit on the number of characters returned in the passphrase.
	 * Should be -1 for no limit. */
	private int charLimit = -1;
	/** A unique but obscured version of the site token. */
	private String key = null;
	/** A reference to the top-level application.  This is primarily used to
	 *  get access to the error string resources of the application and
	 *  certain constants for encryption. */
	private CryptnosApplication theApp = null;
	
	/**
	 * Create a new, empty SiteParameters.  This is primarily intended for
	 * creating SiteParameters from parsing XML.
	 * @param theApp A reference to the full Cryptnos application, used
	 * primarily for notifications
	 */
	public SiteParameters(CryptnosApplication theApp)
	{
		this.theApp = theApp;
	}
	
	/**
	 * Create a new SiteParameters object from individual parameters.
	 * @param theApp A reference to the full Cryptnos application, used
	 * primarily for notifications
	 * @param site The site name or token.
	 * @param charTypes The type of characters to include/exclude from the
	 * final passphrase.
	 * @param charLimit A limit on the number of characters returned in the 
	 * passphrase.  Should be -1 for no limit.
	 * @param hash The cryptographic hash to use to generate the passphrase.
	 * @param iterations The number of iterations of the cryptographic hash to
	 * perform.
	 * @throws Exception Thrown when any parameter is incorrect.
	 */
	public SiteParameters(CryptnosApplication theApp, String site,
			int charTypes, int charLimit, String hash, int iterations)
			throws Exception
	{
		// This is mostly straight forward:
		
		this.theApp = theApp;
		this.site = site;
		this.charTypes = charTypes;
		this.charLimit = charLimit;
		this.hash = hash;
		// Iterations must be a positive integer:
		if (iterations <= 0)
		{
			throw new Exception(theApp.getResources().getString(R.string.error_bad_iterations));
		}
		this.iterations = iterations;
		// The key is generated:
		key = generateKeyFromSite(site, theApp);
	}
	
	/**
	 * Create a new SiteParameters object from encrypted data, presumably
	 * loaded from a database.
	 * @param theApp A reference to the full Cryptnos application, used
	 * primarily for notifications
	 * @param siteKey An obscured site "token" that uniquely identifies the
	 * site parameters in the database.
	 * @param encryptedData A Base64-encoded encrypted string containing the
	 * bulk of the parameter data.
	 * @throws Exception Thrown when any error occurs reconstituting the
	 * encrypted data.
	 */
	public SiteParameters(CryptnosApplication theApp, String siteKey,
			String encryptedData) throws Exception
	{
		// Asbestos underpants:
		try
		{
			this.theApp = theApp;
			// Build our cipher and put it in decrypt mode:
			Cipher cipher = createCipher(siteKey, Cipher.DECRYPT_MODE);
			// Unencrypt the data:
			String combinedParams  =
				new String(cipher.doFinal(Base64.decode(encryptedData.getBytes(theApp.getTextEncoding()))));
			// Split it apart based on the pipe character:
			String[] bits = combinedParams.split("\\|");
			// This should only be valid if we get five inputs:
			if (bits.length == 5)
			{
				// Attempt to decode the data and assign it to the internal
				// values.  Note that we use CryptnosApplication.TEXT_ENCODING_UTF8
				// rather than CryptnosApplication.getTextEncoding() for
				// backward compatibility (we *always* used UTF-8 for this
				// operation).
				site = URLDecoder.decode(bits[0], CryptnosApplication.TEXT_ENCODING_UTF8);
				charTypes = Integer.parseInt(URLDecoder.decode(bits[1], CryptnosApplication.TEXT_ENCODING_UTF8));
				charLimit = Integer.parseInt(URLDecoder.decode(bits[2], CryptnosApplication.TEXT_ENCODING_UTF8));
				iterations = Integer.parseInt(URLDecoder.decode(bits[3], CryptnosApplication.TEXT_ENCODING_UTF8));
				hash = URLDecoder.decode(bits[4], CryptnosApplication.TEXT_ENCODING_UTF8);
				// Generate the site key:
				key = generateKeyFromSite(site, theApp);
			}
			// If we didn't get five parts, something's wrong.  Throw an
			// exception here.  We won't specify anything in the message,
			// however, as this will just be caught by the catch block below.
			else { throw new Exception(); }
		}
		// This should probably be more robust, but for now if any error is
		// thrown, throw a generic exception with a generic error:
		catch (Exception e)
		{
			throw new Exception(theApp.getResources().getString(R.string.error_bad_decrypt));
		}
	}
	
	/**
	 * Create a new SiteParameters object from unencrypted but compacted data,
	 * presumably read from an import file.
	 * @param theApp A reference to the full Cryptnos application, used
	 * primarily for notifications
	 * @param unEncryptedData A String on parameter data combined in a specific
	 * format
	 * @throws Exception Thrown when any error occurs reconstituting the
	 * parameter data.
	 */
	public SiteParameters(CryptnosApplication theApp, String unEncryptedData)
		throws Exception
	{
		// Asbestos underpants:
		try
		{
			this.theApp = theApp;
			// Split it apart based on the pipe character:
			String[] bits = unEncryptedData.split("\\|");
			// This should only be valid if we get five inputs:
			if (bits.length == 5)
			{
				// Attempt to decode the data and assign it to the internal
				// values.  Note the use of CryptnosApplication.TEXT_ENCODING_UTF8
				// rather than CryptnosApplication.getTextEncoding() for
				// backward compatibility.
				site = URLDecoder.decode(bits[0], CryptnosApplication.TEXT_ENCODING_UTF8);
				charTypes = Integer.parseInt(URLDecoder.decode(bits[1], CryptnosApplication.TEXT_ENCODING_UTF8));
				charLimit = Integer.parseInt(URLDecoder.decode(bits[2], CryptnosApplication.TEXT_ENCODING_UTF8));
				iterations = Integer.parseInt(URLDecoder.decode(bits[3], CryptnosApplication.TEXT_ENCODING_UTF8));
				hash = URLDecoder.decode(bits[4], CryptnosApplication.TEXT_ENCODING_UTF8);
				// Generate the site key:
				key = generateKeyFromSite(site, theApp);
			}
			// If we didn't get five parts, something's wrong.  Throw an
			// exception here.  We won't specify anything in the message,
			// however, as this will just be caught by the catch block below.
			else { throw new Exception(); }
		}
		// This should probably be more robust, but for now if any error is
		// thrown, throw a generic exception with a generic error:
		catch (Exception e)
		{
			throw new Exception(theApp.getResources().getString(R.string.error_bad_decrypt));
		}
	}
	
	/** Return the site name or token. */
	public String getSite() { return site; }
	/** Return the character types value. */
	public int getCharTypes() { return charTypes; }
	/** Return the character limit value.  This should be -1 for no limit. */
	public int getCharLimit() { return charLimit; }
	/** Return the cryptographic hash. */
	public String getHash() { return hash; }
	/** Return the number of iterations of the hash to perform. */
	public int getIterations() { return iterations; }
	/** Return the unique, obscured site token. */
	public String getKey() {
		// If the key has already been generated, return it:
		if (key != null) return key;
		// Otherwise, we'll have to generate a new one.  This only makes
		// sense if the site name is not null:
		if (site != null)
		{
			key = generateKeyFromSite(site, theApp);
			return key;
		}
		else return null;
	}
	
	/**
	 * Set the site name or token.
	 * @param site The site name or token string to be used.
	 */
	public void setSite(String site)
	{
		// In addition to setting the site, regenerate the key:
		this.site = site;
		key = generateKeyFromSite(site, theApp);
	}
	
	/**
	 * Set the character types value.
	 * @param charTypes The integer value to set.
	 */
	public void setCharTypes(int charTypes) { this.charTypes = charTypes; }
	
	/**
	 * Set the character limit value.
	 * @param charLimit The integer value to set.
	 */
	public void setCharLimit(int charLimit) { this.charLimit = charLimit; }
	
	/**
	 * Set the cryptographic hash.
	 * @param hash A string containing the name of the cryptographic hash.
	 */
	public void setHash(String hash) { this.hash = hash; }
	
	/**
	 * Set the number of iterations of the cryptographic hash to perform.
	 * @param iterations The integer value to set.  This value must be greater
	 * than zero.
	 * @throws Exception Thrown when the value is less than or equal to zero.
	 */
	public void setIterations(int iterations) throws Exception
	{
		if (iterations > 0) this.iterations = iterations;
		else throw new Exception(theApp.getResources().getString(R.string.error_bad_iterations));
	}
	
	/**
	 * Export the current state of the site parameters as a compact single-line
	 * string of data.  The intended use of this string is to be fed to the
	 * export code, which will combine this value with other site parameters
	 * to create an export file.
	 * @return The encoded string
	 * @throws Exception Thrown if any error occurs during encoding
	 */
	public String exportUnencryptedString() throws Exception
	{
		try
		{
			String combinedParams = 
				URLEncoder.encode(site, CryptnosApplication.TEXT_ENCODING_UTF8) + "|" +
				URLEncoder.encode(Integer.toString(charTypes), CryptnosApplication.TEXT_ENCODING_UTF8) + "|" +
				URLEncoder.encode(Integer.toString(charLimit), CryptnosApplication.TEXT_ENCODING_UTF8) + "|" +
				URLEncoder.encode(Integer.toString(iterations), CryptnosApplication.TEXT_ENCODING_UTF8) + "|" +
				URLEncoder.encode(hash, CryptnosApplication.TEXT_ENCODING_UTF8);
			return combinedParams;
		}
		catch (Exception e)
		{
			throw new Exception(theApp.getResources().getString(R.string.error_bad_encrypt));
		}
	}
	
	/**
	 * Export the current state of the site parameters as a Base64-encoded
	 * encrypted string, suitable for storing in a database.  Use the second
	 * constructor to reconstitute this string into its original form.
	 * @return A Base64-encoded encrypted string.
	 * @throws Exception Throw if an error occurs while encrypting the data.
	 */
	public String exportEncryptedString() throws Exception
	{
		try
		{
			String combinedParams = exportUnencryptedString(); 
			Cipher cipher = createCipher(key, Cipher.ENCRYPT_MODE);
			return base64String(cipher.doFinal(combinedParams.getBytes(theApp.getTextEncoding())));
		}
		catch (Exception e)
		{
			throw new Exception(theApp.getResources().getString(R.string.error_bad_encrypt));
		}
	}
	
	/**
	 * Given the user's secret passphrase, combine it with all the other
	 * site parameters saved within to produce the generated password and
	 * return it to the theApp.
	 * @param secret The user's secret passphrase, which is never stored.
	 * @return A pseudo-random password generated from the site parameters.
	 * @throws Exception Thrown when any error occurs.
	 */
	public String generatePassword(String secret) throws Exception
	{
		try { return generatePassword(secret, null); }
		catch (Exception e) { throw e; }
	}
	
	/**
	 * Given the user's secret passphrase, combine it with all the other
	 * site parameters saved within to produce the generated password and
	 * return it to the theApp.
	 * @param secret The user's secret passphrase, which is never stored.
	 * @param handler If not null, this handler will be notified of the
	 * progress of the generation process, for the purpose of updating a
	 * progress dialog, for example.
	 * @return A pseudo-random password generated from the site parameters.
	 * @throws Exception Thrown when any error occurs.
	 */
	public String generatePassword(String secret, Handler handler) throws Exception
	{
		Message msg = null;
		Bundle b = null;;
		
		// Asbestos underpants:
		try
		{
			// The character limit must be zero or greater, while the
			// iteration count must be one or greater:
			if (charLimit >= 0 && iterations > 0)
			{
				// Concatenate the site and passphrase values, then
				// convert the string to a byte array for hashing:
				byte[] result = site.concat(secret).getBytes(theApp.getTextEncoding());
				// We will use one of two hashing engines.  Internally,
				// Java supports MD5, SHA-1, and a trio of SHA-2 methods.
				// We'll assume that since these are built-in, they must
				// be optimized compared to external definitions.  If
				// the selected hash is one of these, we'll use the
				// internal engine.
				MessageDigest internalHasher = null;
				Digest bcHasher = null;
				if (hash.compareTo("MD5") == 0 ||
					hash.compareTo("SHA-1") == 0 ||
					hash.compareTo("SHA-256") == 0 ||
					hash.compareTo("SHA-384") == 0 ||
					hash.compareTo("SHA-512") == 0) {
					internalHasher = MessageDigest.getInstance(hash);
				}
				// If it is any other engine, we'll fall back to the
				// Bouncy Castle implementations.  We may add more later,
				// but for now we'll only use the same ones supported by
				// the .NET version of Cryptnos.  There, we actually
				// re-implemented their code to fit with the .NET hashing
				// class structure; here, we'll using Bouncy Castle out
				// of the box, so it's trivial to implement them all.
				else if (hash.compareTo("RIPEMD-160") == 0) {
					bcHasher = new RIPEMD160Digest();
				}
				else if (hash.compareTo("Tiger") == 0) {
					bcHasher = new TigerDigest();
				}
				else if (hash.compareTo("Whirlpool") == 0) {
					bcHasher = new WhirlpoolDigest();
				}
				// If we're using the internal hashing engine, we've
				// got things easy.  The most complex part is feeding the
				// hash back into the engine for multiple iterations.
				if (internalHasher != null)
				{
					for (int i = 0; i < iterations; i++) {
						result = internalHasher.digest(result);
						if (handler != null) {
				        	msg = handler.obtainMessage();
			                b = new Bundle();
			                b.putInt("iteration", i);
			                b.putString("password", null);
			                msg.setData(b);
			                handler.sendMessage(msg);

						}
					}
				}
				// If we're using the Bouncy Castle stuff, we'll need to
				// do a bit more work.  Declare the result, feed it to
				// the engine, and get back the hash.  Note that we redeclare
				// the result byte array after the update so each iteration
				// puts the result hash into the array.  Also note that we
				// make sure to reset the engine after each iteration, which
				// apparently the built-in engines do automagically.
				else if (bcHasher != null)
				{
					for (int i = 0; i < iterations; i++)
					{
						bcHasher.update(result, 0, result.length);
						result = new byte[bcHasher.getDigestSize()];
						bcHasher.doFinal(result, 0);
						bcHasher.reset();
						if (handler != null) {
				        	msg = handler.obtainMessage();
			                b = new Bundle();
			                b.putInt("iteration", i);
			                b.putString("password", null);
			                msg.setData(b);
			                handler.sendMessage(msg);

						}
					}
				}
				// By now, we *should* have the intermediate hash in hand.
				// We'll double check with a null check here, just in case.
				if (result != null)
				{
					// Get the raw hash as a Base64 string:
					String b64hash = base64String(result);
					// Now that we've got the hash string, we need to apply
					// our modifications.  First, the character type
					// restriction.  Based on the user's choice in the
					// drop-down, run the hash through some regular
					// expressions to chop out unwanted characters:
					Pattern p = null;
					switch (charTypes)
					{
						// Alphanumerics, change others to underscores
						case 1:
							p = Pattern.compile("\\W");
							b64hash =
								p.matcher(b64hash).replaceAll("_");
							break;
						// Alphanumerics only
						case 2:
							p = Pattern.compile("[^a-zA-Z0-9]");
							b64hash =
								p.matcher(b64hash).replaceAll("");
							break;
						// Alphabetic characters only
						case 3:
							p = Pattern.compile("[^a-zA-Z]");
							b64hash =
								p.matcher(b64hash).replaceAll("");
							break;
						// Numbers only
						case 4:
							p = Pattern.compile("\\D");
							b64hash =
								p.matcher(b64hash).replaceAll("");
							break;
						// By default, use all generated characters
						default:
							break;
					}
					// Next, apply the character limit.  If it's any-
					// thing greater than zero, get only the first so
					// many characters:
					if (charLimit > 0 && b64hash.length() > charLimit)
						b64hash = b64hash.substring(0, charLimit);
					// Now we have our final value.  Display it back
					// to the user and get ready to save it to the
					// database.
					if (handler != null) {
			        	msg = handler.obtainMessage();
		                b = new Bundle();
		                b.putInt("iteration", -100);
		                b.putString("password", b64hash);
		                msg.setData(b);
		                handler.sendMessage(msg);
					}
					return b64hash;
				}
				// If for some reason something failed, the result could
				// be null.  Warn the user as such:
				else
				{
					throw new Exception(theApp.getResources().getString(R.string.error_null_hash));
				}
			}
			// If the iterations or character limit parsing didn't
			// come up roses, show error messages.  Note that there's
			// also a generic "unknown" one here, just in case, but
			// it's probably irrelevant.
			else
			{
				if (iterations <= 0)
					throw new Exception(theApp.getResources().getString(R.string.error_bad_iterations));
				else if (charLimit < 0)
					throw new Exception(theApp.getResources().getString(R.string.error_bad_charlimit));
				else
					throw new Exception(theApp.getResources().getString(R.string.error_unknown));
			}
		}
		// This should probably be more robust, but for now just throw back
		// out any exception we caught:
		catch (Exception e)
		{
			throw e;
		}
	}

	/**
	 * Generate a unique, obscured "site key" from the specified site token
	 * or name.  This key allows us to uniquely identify the site parameters
	 * without revealing the actual site token value.  Note that if an error
	 * occurs while the key is being generated, the site value passed in will
	 * be returned instead, offering no additional security.
	 * @param theSite The site name or token to generate the key from.
	 * @return A Base64-encoded site key string.
	 */
	public static String generateKeyFromSite(String theSite,
			CryptnosApplication theApp)
	{
		// For anyone wondering, note that this is a static method that can
		// be called at any time, given any input.  This is because we may
		// need to generate the key from a site token string without going
		// through the SiteParameter.getKey() or getSite() accessors.  The
		// key parameter uses this method behind the scenes, so the resulting
		// key value should always be the same.
		
		// Asbestos underpants:
		try
		{
			// Make sure the site token isn't null or empty:
			if (theSite != null && theSite.length() > 0)
			{
				// There's a lot of ways we could do this, but perhaps the
				// easiest is to simply hash the string. and return it.
				// Originally, the intent was to salt this with the unique
				// ID of the device
				// (android.provider.Settings.System.ANDROID_ID or
				// android.provider.Settings.Secure.ANDROID_ID), but I
				// screwed up the call and used the identifier string for the
				// property rather than its value.  In order to keep from
				// breaking everything, we'll hard-code that string value here
				// for now, but this really needs to be fixed someday.
				MessageDigest hasher = MessageDigest.getInstance("SHA-512");
				//return base64String(hasher.digest(theSite.concat(Settings.System.ANDROID_ID).getBytes()));
				return base64String(hasher.digest(theSite.concat("android_id").getBytes(theApp.getTextEncoding())));
			}
			else return theSite;
		}
		catch (Exception e) { return theSite; }
	}
	
	/**
	 * Given a byte array, return a Base64-encoded string of its value.  This
	 * method was added because there's no simple, single method way to do
	 * this, and we need to perform this task multiple times.  Abstraction
	 * and code reuse is good. ;)
	 * @param bytes The byte array to encode.
	 * @return A Base64-encoded string.
	 */
	private static String base64String(byte[] bytes)
	{
		// Asbestos underpants:
		try
		{
			// Seriously?  There's no easier way to do this?  Not even with
			// Bouncy Castle's stuff?  We'll take advantage of Bouncy Castle
			// doing the brunt of the work, but we still need to go through
			// the trouble of using a ByteArrayOutputStream to do all this.
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			Base64.encode(bytes, baos);
			baos.flush();
			baos.close();
			return baos.toString();
		}
		// Not sure what could occur here, but just in case, we'll return
		// a simple null if anything blows up.
		catch (Exception e) { return null; }
	}
	
	/**
	 * Create the cipher used to encrypt or decrypt site parameter data.
	 * @param password A "secret" String value, usually the derived site
	 * "key".  This is specified as an input parameter rather than using the
	 * member variable because this method will be needed for one of the
	 * constructors.
	 * @param mode The Cipher encrypt/decryption mode.  This should be either
	 * Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE.
	 * @return A Cipher suitable for the encryption/decryption task.
	 * @throws Exception Thrown if the mode is invalid or if any error occurs
	 * while creating the cipher.
	 */
	private static Cipher createCipher(String password, int mode)
		throws Exception
	{
		// Asbestos underpants:
		try
		{
			// I had a devil of a time getting this to work, but I eventually
			// peeked at the Google "Secrets" application source code to get
			// to this setup.  The Password Based Key (PBE) spec lets us
			// specify a password to generate keys from.  We'll use the key
			// passed in (most likely a "site key" from the site parameters)
			// as that password, salting it with the device's unique ID to
			// give it some uniqueness from device to device.
			PBEKeySpec pbeKeySpec =	new PBEKeySpec(password.toCharArray(),
				CryptnosApplication.PARAMETER_SALT,
				CryptnosApplication.KEY_ITERATION_COUNT,
				CryptnosApplication.KEY_LENGTH);
			// Next we'll need a key factory to actually build the key:
			SecretKeyFactory keyFac =
				SecretKeyFactory.getInstance(CryptnosApplication.KEY_FACTORY);
			// The key is generated from the key factory:
			SecretKey key = keyFac.generateSecret(pbeKeySpec);
			// The cipher needs some parameter specs to know how to use
			// the key:
			AlgorithmParameterSpec aps =
				new PBEParameterSpec(CryptnosApplication.PARAMETER_SALT,
				CryptnosApplication.KEY_ITERATION_COUNT);
			// Now that we have all of this information, actually start
			// creating the cipher:
			Cipher cipher = Cipher.getInstance(CryptnosApplication.KEY_FACTORY);
			// For our purposes, we're combining the creation of encryption
			// and decryption ciphers into one method.  So take the mode
			// passed in and initialize the cipher based on that mode.  Note
			// that the key and parameter specs are being pulled in at this
			// point, making the cipher complete.  If we get an invalid mode
			// type, throw an error.
			switch (mode)
			{
				case Cipher.ENCRYPT_MODE:
					cipher.init(Cipher.ENCRYPT_MODE, key, aps);
					break;
				case Cipher.DECRYPT_MODE:
					cipher.init(Cipher.DECRYPT_MODE, key, aps);
					break;
				default:
					throw new Exception("Invalid cipher mode");
			}
			// By now our cipher *should* be ready.  Go ahead an return it:
			return cipher;
		}
		// If anything blew up, throw it back out:
		catch (Exception e)
		{
			throw e;
		}
	}
	
}
